<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Hacker News, filtered</title>
  <style>
    :root {
      font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
      --accent: #ff6600;
    }
    body {
      margin: 0 auto;
      max-width: 800px;
      padding: 1rem;
      line-height: 1.4;
    }
    h1 {
      color: var(--accent);
      margin: 0 0 1rem;
      font-size: 1.5rem;
    }
    #controls {
      display: flex;
      flex-wrap: wrap;
      gap: 0.5rem;
      margin-bottom: 1rem;
      align-items: center;
    }
    #filterInput {
      flex: 1 1 250px;
      padding: 0.5rem;
      font-size: 1rem;
    }
    button {
      padding: 0.5rem 0.75rem;
      font-size: 1rem;
      border: 1px solid var(--accent);
      background: var(--accent);
      color: white;
      cursor: pointer;
      border-radius: 4px;
    }
    button:disabled {
      opacity: 0.6;
      cursor: default;
    }
    ul {
      list-style: none;
      padding: 0;
      margin: 0;
    }
    li {
      padding: 0.5rem 0;
      border-bottom: 1px solid #eee;
    }
    li a {
      color: inherit;
      text-decoration: none;
    }
    li a:hover {
      text-decoration: underline;
    }
    .meta {
      font-size: 0.85rem;
      color: #555;
    }
    .meta a {
      color: #555;
    }
    .loading {
      color: #999;
      font-style: italic;
    }
  </style>
</head>
<body>
  <h1>Hacker News, filtered</h1>
  <div id="controls">
    <label for="filterInput">Exclude terms (comma‑separated):</label>
    <input id="filterInput" type="text" placeholder="llm, ai, agent" />
    <button id="refreshBtn">Refresh</button>
  </div>
  <p id="status">Loading…</p>
  <ul id="stories"></ul>

  <script>
    const API_TOP = 'https://hacker-news.firebaseio.com/v0/topstories.json';
    const API_ITEM = id => `https://hacker-news.firebaseio.com/v0/item/${id}.json`;
    const POSTS_TO_FETCH = 200;
    const LS_KEY = "hnFilterTerms";

    const filterInput = document.getElementById("filterInput");
    const refreshBtn = document.getElementById("refreshBtn");
    const statusEl = document.getElementById("status");
    const storiesEl = document.getElementById("stories");

    let allStories = []; // Store all fetched stories

    function loadFilterTerms() {
      const saved = localStorage.getItem(LS_KEY);
      return saved ? saved.split(/\s*,\s*/).filter(Boolean) : ["llm", "ai"];
    }

    function saveFilterTerms(terms) {
      localStorage.setItem(LS_KEY, terms.join(", "));
    }

    function getFilterTerms() {
      return filterInput.value.trim().split(/\s*,\s*/).filter(Boolean).map(t => t.toLowerCase());
    }

    function msSince(dateStr) {
      return Date.now() - new Date(dateStr).getTime();
    }

    function timeAgo(dateStr) {
      const ms = msSince(dateStr);
      const minutes = Math.floor(ms / 60000);
      if (minutes < 60) return `${minutes}m ago`;
      const hours = Math.floor(minutes / 60);
      if (hours < 24) return `${hours}h ago`;
      const days = Math.floor(hours / 24);
      return `${days}d ago`;
    }

    function unixToDateString(unixSeconds) {
      return new Date(unixSeconds * 1000).toISOString();
    }

    function escapeHTML(str) {
      if (!str) return '';
      return str.replace(/[&<>"']/g, c => ({
        '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
      }[c]));
    }

    async function fetchStory(id) {
      try {
        const res = await fetch(API_ITEM(id));
        const story = await res.json();
        return story;
      } catch (err) {
        console.error(`Failed to fetch story ${id}:`, err);
        return null;
      }
    }

    async function loadTopStories() {
      statusEl.textContent = "Loading top stories…";
      refreshBtn.disabled = true;
      
      try {
        // Fetch top story IDs
        const res = await fetch(API_TOP);
        const ids = await res.json();
        
        // Fetch story details for the first POSTS_TO_FETCH stories
        const storyPromises = ids.slice(0, POSTS_TO_FETCH).map(fetchStory);
        
        statusEl.textContent = `Loading ${POSTS_TO_FETCH} stories…`;
        
        // Wait for all stories to load
        const stories = await Promise.all(storyPromises);
        
        // Filter out null/failed stories and stories without titles
        allStories = stories.filter(story => story && story.title);
        
        // Apply current filter and render
        applyFilterAndRender();
        
      } catch (err) {
        console.error('Failed to load stories:', err);
        statusEl.textContent = "Failed to fetch stories.";
        allStories = [];
      } finally {
        refreshBtn.disabled = false;
      }
    }

    function filterStories(stories, terms) {
      if (!terms.length) return stories;
      return stories.filter(story => {
        const haystack = `${story.title} ${story.url || ""} ${story.text || ""}`.toLowerCase();
        return !terms.some(term => haystack.includes(term));
      });
    }

    function renderStories(stories) {
      storiesEl.innerHTML = "";
      
      if (!stories.length) {
        statusEl.textContent = allStories.length ? "No stories after applying filter." : "No stories loaded.";
        return;
      }
      
      statusEl.textContent = `${stories.length} stories shown (of ${allStories.length} loaded)`;
      
      const frag = document.createDocumentFragment();
      stories.forEach(story => {
        const li = document.createElement("li");
        
        const a = document.createElement("a");
        a.href = story.url || `https://news.ycombinator.com/item?id=${story.id}`;
        a.target = "_blank";
        a.rel = "noopener";
        a.textContent = story.title;
        li.appendChild(a);

        const meta = document.createElement("div");
        meta.className = "meta";
        const dateString = unixToDateString(story.time);
        meta.innerHTML = `${story.score || 0} points by <a href="https://news.ycombinator.com/user?id=${story.by}" target="_blank" rel="noopener">${escapeHTML(story.by)}</a> | <a href="https://news.ycombinator.com/item?id=${story.id}" target="_blank" rel="noopener">${story.descendants || 0} comments</a> | <span title="${dateString}">${timeAgo(dateString)}</span>`;
        li.appendChild(meta);
        
        frag.appendChild(li);
      });
      storiesEl.appendChild(frag);
    }

    function applyFilterAndRender() {
      const terms = getFilterTerms();
      saveFilterTerms(terms);
      const filteredStories = filterStories(allStories, terms);
      renderStories(filteredStories);
    }

    async function refresh() {
      await loadTopStories();
    }

    // Init
    document.addEventListener("DOMContentLoaded", () => {
      // Load saved filters
      const initialTerms = loadFilterTerms();
      filterInput.value = initialTerms.join(", ");
      refresh();
    });

    // Listeners
    refreshBtn.addEventListener("click", refresh);
    filterInput.addEventListener("keyup", (e) => {
      if (e.key === "Enter") {
        applyFilterAndRender(); // Just re-filter existing stories
      }
    });
  </script>
</body>
</html>
